/************************************************************************
                                                                     
   Xceed Ultimate ListBox for Silverlight                                                                                                                                            
   Copyright (C) 2010 Xceed Software Inc.    
                                                                     
   This program is provided to you under the terms of the GNU General Public  
   License version 2 as published by the Free Software Foundation. 
        
   This program is distributed in the hope that it will be useful, but
   WITHOUT ANY WARRANTY, without even the implied warranty of 
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
   General Public License for more details.

   You should have received a copy of the GNU General Public License along 
   with this program, if not, write to the Free Software Foundation, Inc., 
   51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

   This program can be provided to you by Xceed Software Inc. under a
   proprietary commercial license agreement for use in non-Open Source
   projects. Visit Xceed.com to find the commercial edition of 
   Xceed Ultimate ListBox for Silverlight.                                    
                                                                      
 **********************************************************************/

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.ServiceModel.DomainServices.Client;
using System.Threading;
using Xceed.Silverlight.Data.Stats;

namespace Xceed.Silverlight.Data
{
  public abstract class WcfRiaServicesDataSourceProviderBase<TEntity> : AsyncDataSourceProvider, ICountRestrictedProvider where TEntity : Entity
  {
    #region Constructors

    static WcfRiaServicesDataSourceProviderBase()
    {
      SharedMetadata.DisplayAttributeAssemblyQualifiedName = typeof( DisplayAttribute ).AssemblyQualifiedName;
    }

    protected WcfRiaServicesDataSourceProviderBase(
      EntityQuery<TEntity> entityQuery,
      DomainContext domainContext )
    {
      if( entityQuery == null )
        throw new ArgumentNullException( "entityQuery" );

      if( domainContext == null )
        throw new ArgumentNullException( "domainContext" );

      this.EntityQuery = entityQuery;
      this.DomainContext = domainContext;
    }

    #endregion

    #region CanSkip Property

    public override bool CanSkip
    {
      get
      {
        return true;
      }
    }

    #endregion

    #region CanFilter Property

    public override bool CanFilter
    {
      get
      {
        return true;
      }
    }

    #endregion

    #region CanFilterGroup Property

    public sealed override bool CanFilterGroup
    {
      get
      {
        return false;
      }
    }

    #endregion

    #region CanJump Property

    public sealed override bool CanJump
    {
      get
      {
        return false;
      }
    }

    #endregion

    #region CanJumpToItem Property

    public sealed override bool CanJumpToItem
    {
      get
      {
        return false;
      }
    }

    #endregion

    #region CanCount Property

    public override bool CanCount
    {
      get
      {
        return this.EntityQuery.IncludeTotalCount;
      }
    }

    #endregion

    #region CanGroupCount Property

    public sealed override bool CanGroupCount
    {
      get
      {
        return false;
      }
    }

    #endregion

    #region CanCalculateStats Property

    public override bool CanCalculateStats
    {
      get
      {
        return this.CanCount;
      }
    }

    #endregion

    #region InitialItemCount Property

    public Nullable<long> InitialItemCount
    {
      get;
      set;
    }

    #endregion

    #region ElementType Protected Property

    protected internal sealed override Type ElementType
    {
      get
      {
        return this.EntityQuery.EntityType;
      }
    }

    #endregion

    #region EntityQuery Protected Property

    protected EntityQuery<TEntity> EntityQuery
    {
      get;
      private set;
    }

    #endregion

    #region DomainContext Protected Property

    protected DomainContext DomainContext
    {
      get;
      private set;
    }

    #endregion

    #region IsQueryLengthLimited Internal Property

    internal override bool IsQueryLengthLimited
    {
      get
      {
        return true;
      }
    }

    #endregion

    protected override IEnumerable<SortDescription> GetPrimaryKeyInfo()
    {
      Type entityType = this.ElementType;
      Type attributeType = typeof( KeyAttribute );

      foreach( PropertyInfo propertyInfo in entityType.GetProperties() )
      {
        if( propertyInfo.IsDefined( attributeType, true ) )
        {
          string memberName = string.Intern( propertyInfo.Name );

          yield return new SortDescription( memberName, ListSortDirection.Ascending );
        }
      }

      foreach( FieldInfo fieldInfo in entityType.GetFields() )
      {
        if( fieldInfo.IsDefined( attributeType, true ) )
        {
          string memberName = string.Intern( fieldInfo.Name );

          yield return new SortDescription( memberName, ListSortDirection.Ascending );
        }
      }
    }

    protected override IEnumerable<Type> GetSupportedStatFunctions( Type dataType )
    {
      // Check if we can replace the CountFunction with a call to BeginExecuteCountQuery.
      if( this.CanCount )
      {
        bool mayCount = !dataType.IsNullableType();
        if( ( !mayCount ) && ( this.CanFilter ) )
        {
          // Since the CountFunction may require null values inclusion or exclusion, we need
          // to be able set a filter for null or non-null values.
          List<FilterOperator> filterOperators = this.GetSupportedFilterOperators( dataType ).ToList();

          mayCount = ( filterOperators.Contains( FilterOperator.Equal ) )
                  && ( filterOperators.Contains( FilterOperator.NotEqual ) );
        }

        if( mayCount )
          yield return typeof( CountFunction );
      }

      yield break;
    }

    protected virtual EntityQuery<TEntity> GetEntityQuery( CountQueryParameters queryParameters )
    {
      Debug.Assert( ( queryParameters.FilterGroup == null )
                 && ( queryParameters.Jump == null ) );

      EntityQuery<TEntity> entityQuery = this.EntityQuery;

      if( queryParameters.Filter != null )
      {
        SortRequest sortBy = ( queryParameters.Sort != null )
                               ? queryParameters.Sort
                               : new SortRequest( this.PrimaryKeyInfo );

        entityQuery = WcfRiaServicesDataSourceProviderBase<TEntity>.OrderBy( entityQuery, sortBy );
        entityQuery = WcfRiaServicesDataSourceProviderBase<TEntity>.Where( entityQuery, queryParameters.Filter );
      }

      entityQuery = ( EntityQuery<TEntity> )entityQuery.Take( 0 );

      return entityQuery;
    }

    protected virtual EntityQuery<TEntity> GetEntityQuery( DataQueryParameters queryParameters )
    {
      Debug.Assert( ( queryParameters.ItemJump == null )
                 && ( queryParameters.Jump == null ) );

      EntityQuery<TEntity> entityQuery = this.EntityQuery;

      entityQuery = WcfRiaServicesDataSourceProviderBase<TEntity>.OrderBy( entityQuery, queryParameters.Sort );

      if( queryParameters.Filter != null )
      {
        if( queryParameters.Sort == null )
        {
          entityQuery = WcfRiaServicesDataSourceProviderBase<TEntity>.OrderBy( entityQuery, new SortRequest( this.PrimaryKeyInfo ) );
        }

        entityQuery = WcfRiaServicesDataSourceProviderBase<TEntity>.Where( entityQuery, queryParameters.Filter );
      }

      entityQuery = WcfRiaServicesDataSourceProviderBase<TEntity>.Skip( entityQuery, queryParameters.Skip );
      entityQuery = WcfRiaServicesDataSourceProviderBase<TEntity>.Take( entityQuery, queryParameters.Take );

      return entityQuery;
    }

    protected override IAsyncResult BeginExecuteCountQuery(
      CountQueryParameters queryParameters,
      AsyncCallback callback,
      object state )
    {
      EntityQuery<TEntity> entityQuery = this.GetEntityQuery( queryParameters );

      LoadAsyncResult asyncResult = new LoadAsyncResult( callback, state );

      this.DomainContext.Load(
        entityQuery,
        LoadBehavior.MergeIntoCurrent,
        this.OnCountLoadCompleted,
        asyncResult );

      asyncResult.IsSynchronous = false;

      return asyncResult;
    }

    protected override CountQueryResult EndExecuteCountQuery( IAsyncResult asyncResult )
    {
      LoadAsyncResult loadAsyncResult = ( LoadAsyncResult )asyncResult;

      return ( CountQueryResult )loadAsyncResult.Result;
    }

    protected override IAsyncResult BeginExecuteDataQuery(
      DataQueryParameters queryParameters,
      AsyncCallback callback,
      object state )
    {
      EntityQuery<TEntity> entityQuery = this.GetEntityQuery( queryParameters );

      LoadAsyncResult asyncResult = new LoadAsyncResult( callback, state );

      this.DomainContext.Load(
        entityQuery,
        LoadBehavior.MergeIntoCurrent,
        this.OnDataLoadCompleted,
        asyncResult );

      asyncResult.IsSynchronous = false;

      return asyncResult;
    }

    protected override DataQueryResult EndExecuteDataQuery( IAsyncResult asyncResult )
    {
      LoadAsyncResult loadAsyncResult = ( LoadAsyncResult )asyncResult;

      return ( DataQueryResult )loadAsyncResult.Result;
    }

    protected override IAsyncResult BeginExecuteGroupCountQuery(
      GroupCountQueryParameters queryParameters,
      AsyncCallback callback,
      object state )
    {
      throw new NotSupportedException();
    }

    protected override GroupCountQueryResult EndExecuteGroupCountQuery( IAsyncResult asyncResult )
    {
      throw new NotSupportedException();
    }

    protected override IAsyncResult BeginExecuteStatQuery(
      StatQueryParameters queryParameters,
      AsyncCallback callback,
      object state )
    {
      CountFunction countFunction = queryParameters.Function.StatFunction as CountFunction;

      if( countFunction == null )
        // The StatFunction isn't supported.
        throw new NotSupportedException();

      // The CountFunction may be handled by a call to BeginExecuteCountQuery.
      FilterRequest filterRequest = queryParameters.Filter;
      FilterExpressionBase filterExpression = ( filterRequest != null ) ? filterRequest.Expression : null;
      string propertyName = countFunction.GetSourcePropertyNames().Single();
      bool isNullableType = this.GetElementMemberType( propertyName ).IsNullableType();

      // Modify the filter expression to take care of the null value.
      if( isNullableType )
      {
        if( countFunction.IncludeNullValues )
        {
          FilterExpression includeNull = new FilterExpression( propertyName, FilterOperator.Equal, null );

          if( filterExpression != null )
          {
            filterExpression = new OrFilterExpression( includeNull, filterExpression );
          }
          else
          {
            filterExpression = includeNull;
          }
        }
        else
        {
          FilterExpression excludeNull = new FilterExpression( propertyName, FilterOperator.NotEqual, null );

          if( filterExpression != null )
          {
            filterExpression = new AndFilterExpression( excludeNull, filterExpression );
          }
          else
          {
            filterExpression = excludeNull;
          }
        }
      }

      // Create the new FilterRequest for the count query.
      if( filterRequest != null )
      {
        filterRequest = new FilterRequest( filterRequest.Filter, filterExpression );
      }
      else if( filterExpression != null )
      {
        filterRequest = new FilterRequest( filterExpression );
      }
      else
      {
        filterRequest = null;
      }

      // Start the count query.
      CountQueryParameters newQueryParameters = new CountQueryParameters(
                                                  queryParameters.Sort,
                                                  filterRequest,
                                                  null,
                                                  queryParameters.FilterGroup );

      return this.BeginExecuteCountQuery( newQueryParameters, callback, state );
    }

    protected override StatQueryResult EndExecuteStatQuery( IAsyncResult asyncResult )
    {
      CountQueryResult queryResult = this.EndExecuteCountQuery( asyncResult );

      return new StatQueryResult( new StatResult( queryResult.Count ) );
    }

    private static EntityQuery<TEntity> Take(
      EntityQuery<TEntity> entityQuery,
      TakeRequest request )
    {
      if( request == null )
        return entityQuery;

      return entityQuery.Take<TEntity>( request.Count );
    }

    private static EntityQuery<TEntity> Skip(
      EntityQuery<TEntity> entityQuery,
      SkipRequest request )
    {
      if( ( request == null ) || ( request.Count == 0 ) )
        return entityQuery;

      long skip = request.Count;

      while( skip > 0 )
      {
        if( skip > int.MaxValue )
        {
          entityQuery = entityQuery.Skip<TEntity>( int.MaxValue );
          skip -= int.MaxValue;
        }
        else
        {
          entityQuery = entityQuery.Skip<TEntity>( Convert.ToInt32( skip ) );
          skip = 0;
        }
      }

      return entityQuery;
    }

    private static EntityQuery<TEntity> Where(
      EntityQuery<TEntity> entityQuery,
      FilterRequest request )
    {
      if( request == null )
        return entityQuery;

      Debug.Assert( ( request.Filter == null ) && ( request.Expression != null ) );

      PredicateExpressionBuilder pb = new PredicateExpressionBuilder( request.Expression );
      Expression<Func<TEntity, bool>> predicate = pb.ToExpression<TEntity>();

      return entityQuery.Where<TEntity>( predicate );
    }

    private static EntityQuery<TEntity> OrderBy(
      EntityQuery<TEntity> entityQuery,
      SortRequest request )
    {
      if( ( request == null ) || ( request.SortDescriptions.Count <= 0 ) )
        return entityQuery;

      bool isFirstSort = true;
      Type entityType = entityQuery.EntityType;

      foreach( SortDescription sort in request.SortDescriptions )
      {
        LambdaExpression keySelector = WcfRiaServicesDataSourceProviderBase<TEntity>.CreateKeySelectorExpression( sort.PropertyName, entityType );
        Type[] keySelectorGenericType = null;

        if( keySelector.Type.IsGenericType )
        {
          keySelectorGenericType = keySelector.Type.GetGenericArguments();
        }

        Debug.Assert( ( keySelectorGenericType != null ) && ( keySelectorGenericType.Length == 2 ) );

        MethodInfo sortingMethod;

        if( sort.Direction == ListSortDirection.Ascending )
        {
          sortingMethod = ( isFirstSort )
                            ? WcfRiaServicesDataSourceProviderBase<TEntity>.FindEntityQueryableMethod( "OrderBy", keySelectorGenericType )
                            : WcfRiaServicesDataSourceProviderBase<TEntity>.FindEntityQueryableMethod( "ThenBy", keySelectorGenericType );
        }
        else
        {
          sortingMethod = ( isFirstSort )
                            ? WcfRiaServicesDataSourceProviderBase<TEntity>.FindEntityQueryableMethod( "OrderByDescending", keySelectorGenericType )
                            : WcfRiaServicesDataSourceProviderBase<TEntity>.FindEntityQueryableMethod( "ThenByDescending", keySelectorGenericType );
        }

        isFirstSort = false;

        entityQuery = ( EntityQuery<TEntity> )sortingMethod.Invoke(
                                                entityQuery,
                                                new object[] { 
                                                  entityQuery, 
                                                  keySelector } );
      }

      return entityQuery;
    }

    private static LambdaExpression CreateKeySelectorExpression(
      string propertyName,
      Type entityType )
    {
      ParameterExpression parameter = Expression.Parameter( entityType );
      MemberExpression member = Expression.PropertyOrField( parameter, propertyName );

      return Expression.Lambda( member, parameter );
    }

    private static MethodInfo FindEntityQueryableMethod(
      string methodName,
      params Type[] genericTypes )
    {
      MethodInfo methodInfo = typeof( EntityQueryable ).GetMethod( methodName );

      if( methodInfo != null )
      {
        if( methodInfo.IsGenericMethod )
        {
          methodInfo = methodInfo.MakeGenericMethod( genericTypes );
        }
        else
        {
          methodInfo = null;
        }
      }

      if( methodInfo == null )
        throw new ArgumentException( "The specified method was not found on the EntityQueryable class." );

      return methodInfo;
    }

    private bool TryGetInlineCount( LoadOperation<TEntity> operation, out long inlineCount )
    {
      inlineCount = 0;

      bool found = false;
      long count = operation.TotalEntityCount;

      // A LoadOperation may give a negative entity count value to identify a special state.
      if( count >= 0 )
      {
        inlineCount = count;
        found = true;
      }

      return found;
    }

    private void OnCountLoadCompleted( LoadOperation<TEntity> operation )
    {
      LoadAsyncResult asyncResult = ( LoadAsyncResult )operation.UserState;
      long totalCount = 0;

      if( operation.HasError )
      {
        asyncResult.Error = operation.Error;
      }
      else if( !this.TryGetInlineCount( operation, out totalCount ) )
      {
        try
        {
          // The exception must be thrown on the call to EndExecuteCountQuery.
          // We throw the exception within a try/catch block to get a call stack.
          throw new InvalidOperationException( "The total item count was not provided by the server." );
        }
        catch( Exception e )
        {
          asyncResult.Error = e;
        }
      }

      asyncResult.Result = new CountQueryResult( totalCount );
      asyncResult.Complete();
    }

    private void OnDataLoadCompleted( LoadOperation<TEntity> operation )
    {
      LoadAsyncResult asyncResult = ( LoadAsyncResult )operation.UserState;
      Nullable<long> inlineCount = null;
      long count = 0;

      if( operation.HasError )
      {
        asyncResult.Error = operation.Error;
      }
      else if( this.TryGetInlineCount( operation, out count ) )
      {
        inlineCount = count;
      }

      asyncResult.Result = new DataQueryResult( operation.Entities, inlineCount );
      asyncResult.Complete();
    }

    #region Private LoadAsyncResult Nested Type

    private class LoadAsyncResult : IAsyncResult
    {
      internal LoadAsyncResult( AsyncCallback callback, object state )
      {
        m_callback = callback;
        m_state = state;
      }

      internal bool IsSynchronous
      {
        get
        {
          return m_isSynchronous;
        }
        set
        {
          if( !m_isCompleted )
          {
            m_isSynchronous = value;
          }
        }
      }

      internal object Result
      {
        get
        {
          return m_result;
        }
        set
        {
          if( !m_isCompleted )
          {
            m_result = value;
          }
        }
      }

      internal Exception Error
      {
        get
        {
          return m_error;
        }
        set
        {
          if( !m_isCompleted )
          {
            m_error = value;
          }
        }
      }

      object IAsyncResult.AsyncState
      {
        get
        {
          return m_state;
        }
      }

      WaitHandle IAsyncResult.AsyncWaitHandle
      {
        get
        {
          lock( m_lock )
          {
            if( m_waitHandle == null )
            {
              m_waitHandle = new ManualResetEvent( m_isCompleted );
            }
          }

          return m_waitHandle;
        }
      }

      bool IAsyncResult.CompletedSynchronously
      {
        get
        {
          return m_isCompleted && m_isSynchronous;
        }
      }

      bool IAsyncResult.IsCompleted
      {
        get
        {
          return m_isCompleted;
        }
      }


      internal void Complete()
      {
        lock( m_lock )
        {
          if( m_isCompleted )
            return;

          m_isCompleted = true;

          if( m_waitHandle != null )
          {
            m_waitHandle.Set();
          }
        }

        if( m_callback != null )
        {
          m_callback.Invoke( this );
        }
      }


      private AsyncCallback m_callback; //null
      private object m_state; //null
      private ManualResetEvent m_waitHandle; //null
      private bool m_isCompleted; //false
      private bool m_isSynchronous = true;
      private object m_result; //null
      private Exception m_error; //null
      private object m_lock = new object();
    }

    #endregion
  }
}
